import { createListenerRegistry, Registry, Listener } from '@/common/registry'
import { Macro, MacroRunningStatus, MacroStatus, CommandRunningStatus } from '@/types/macro'
import { uid } from '@/common/ts_utils'
import { CallStackElement, CallStackOption, Updater, CallStackEvent, FrameStatus } from '@/types/call_stack'

export class CallStackError extends Error { }
// 调用栈
export class CallStack {
  protected list: CallStackElement<Macro, MacroRunningStatus>[] = []
  protected opts: CallStackOption<Macro, MacroRunningStatus>
  protected registry: Registry<Listener>

  constructor(options: CallStackOption<Macro, MacroRunningStatus>) {
    this.opts = options
    this.registry = createListenerRegistry()
  }

  public clear(): void {
    this.list = []
  }

  public toArray(): CallStackElement<Macro, MacroRunningStatus>[] {
    return [...this.list]
  }

  public getCount(): number {
    return this.list.length
  }

  public isEmpty(): boolean {
    return this.getCount() === 0
  }

  public contains(item: CallStackElement<Macro, MacroRunningStatus>): boolean {
    return !!this.list.find((el) => el === item)
  }

  public push(item: CallStackElement<Macro, MacroRunningStatus>): void {
    this.list.push(item)
  }

  public peek(): CallStackElement<Macro, MacroRunningStatus> {
    this.guardNotEmpty()
    return this.list[this.getCount() - 1]
  }

  public bottom(): CallStackElement<Macro, MacroRunningStatus> {
    this.guardNotEmpty()
    return this.list[0]
  }

  public pop(): CallStackElement<Macro, MacroRunningStatus> {
    this.guardNotEmpty()
    return this.list.pop() as CallStackElement<Macro, MacroRunningStatus>
  }

  protected guardNotEmpty(): void {
    if (this.isEmpty()) {
      throw new CallStackError('empty stack')
    }
  }

  public on(eventName: CallStackEvent, listener: Listener): (() => void) {
    this.registry.add(eventName, listener)
    return () => this.registry.remove(eventName, listener)
  }

  public off(): void {
    this.registry.destroy()
  }

  // 调用栈
  // 将用例压入栈
  public call(resource: Macro, runningStatus?: MacroRunningStatus): Promise<void> {
    if (!this.isEmpty()) {
      // 栈不为空，更新栈顶的用例状态
      this.updatePeek((element: CallStackElement<Macro, MacroRunningStatus>): CallStackElement<Macro, MacroRunningStatus> => {
        return {
          id: element.id,
          resource: element.resource,
          runningStatus: {
            ...this.opts.updateRunningStatusForCaller(element.runningStatus, element.resource),
            status: MacroStatus.Calling
          }
        }
      })
    }

    // 压入栈
    this.push({
      resource,
      id: uid(),
      runningStatus: runningStatus || this.opts.getInitialRunningStatus(resource)
    })
    // 发通知
    this.registry.fire(CallStackEvent.BeforeRun, this.callStackSnapshot())
    return this.runPeek()
  }

  // 执行栈顶的用例
  protected runPeek(isResume: boolean = false): Promise<void> {

    // 准备栈顶的上下文
    return this.opts.prepareContext(this.peek(), this.getFrameStatus(this.peek(), isResume))
      .then(() => {
        const item = this.peek()

        // ** HERE is where macro starts to run
        // 执行栈顶的用例
        return this.opts.run(item, this.getFrameStatus(item, isResume))
          .then(() => {
            // 快照
            const snapshot = this.callStackSnapshot();
            // 获取最后的frameId
            const last = snapshot[snapshot.length - 1]
            const latestFrameId = last ? last.frameId : null;
            // FIXME: There is a kind of design error here. This callback after runPeek could be run twice
            // for a single macro.
            //
            // For exmaple, Run A => A calls B => B returns to A => A continues
            // 1) Run A: runPeek is called for A for first time
            // 2) A calls B: runPeek is called for B for first time
            // 3) B returns to A: The call to runPeek below is run, so runPeek is called for A for second time
            //
            // As a temporary fix, I have to check if snapshot's last element is the same as the one being run
            if (latestFrameId !== item.id) {
              return
            }

            // 发通知
            this.registry.fire(CallStackEvent.AfterReturn, snapshot);
            // 弹出栈
            this.pop();

            if (this.isEmpty()) {
              // 栈为空结束
              return
            }

            // 更新栈顶
            this.updatePeekAfterResume();

            // 发通知
            this.registry.fire(CallStackEvent.BeforeResume, this.callStackSnapshot());
            return this.runPeek(true);
          })
      })
  }

  protected updatePeek(updater: Updater): void {
    this.guardNotEmpty()
    const index = this.getCount() - 1
    this.list[index] = updater(this.list[index])
  }

  protected getFrameStatus(element: CallStackElement<Macro, MacroRunningStatus>, isResume: boolean): FrameStatus {
    return {
      isResume,
      isBottom: this.getCount() === 1,
      frameId: element.id
    }
  }

  public isAtBottom(): boolean {
    return this.getCount() === 1
  }

  protected callStackSnapshot() {
    return this.list.map((item) => ({
      id: item.resource.id,
      name: item.resource.name,
      frameId: item.id
    }))
  }

  // 更新栈顶
  protected updatePeekAfterResume() {
    this.updatePeek((element: CallStackElement<Macro, MacroRunningStatus>): CallStackElement<Macro, MacroRunningStatus> => {
      const curIndex = element.runningStatus.nextIndex
      const curId = element.resource.commands[curIndex].id
      const nextIndex = curIndex + 1
      const commandResults = [...element.runningStatus.commandResults]

      commandResults[curIndex] = CommandRunningStatus.Success

      return {
        id: element.id,
        resource: element.resource,
        runningStatus: {
          nextIndex,
          commandResults,
          status: MacroStatus.Running,
          playerState: {
            ...element.runningStatus.playerState,
            nextIndex,
            doneIds: [
              ...element.runningStatus.playerState.doneIds,
              curId
            ]
          },
          interpreterState: element.runningStatus.interpreterState
        }
      }
    })
  }

}
